#include "client.h"
#include <curl/curl.h>
#include "common/str.h"
#include <cstring>

#include "common/log.h"


namespace evm { namespace http {

void Client::request(Request &req, Response& res, size_t timeout) {
    CURL *curl = curl_easy_init();
    if (!curl)
        throw runtime_error("curl_easy_init failed");

    curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent);
    if (req.query_paras.empty())
        curl_easy_setopt(curl, CURLOPT_URL, req.url.data());
    else {
        string url = req.url;
        for (auto& item: req.query_paras)
            url += "&" + item.first + "=" + item.second;
        url[req.url.size()] = '?';  // modify first '&' to '?'
        curl_easy_setopt(curl, CURLOPT_URL, url.data());
    }

    string body;
    if (!req.json.is_null()) {
        body = req.json.dump();
        req.set_header("Content-type", "application/json");
    }
    else
        body = req.body;

    log_info("request: {}", req.str());

    ReadObject read_object;
    if (req.has_file) {
        read_object.file = new File(req.file_name);
        read_object.len = read_object.file->size();
    }/* else {
        read_object.data = body.data();
        read_object.len = body.size();
    }*/

    switch (req.method) {
        case GET:
            break;
        case POST: {
            curl_easy_setopt(curl, CURLOPT_POST, 1);
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.data());
            curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size());
        }
            break;
        case PUT: {
            if (read_object.file) {
                curl_easy_setopt(curl, CURLOPT_PUT, 1);
                curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
                curl_easy_setopt(curl, CURLOPT_READFUNCTION, Client::read_callback);
                curl_easy_setopt(curl, CURLOPT_READDATA, &read_object);
                curl_easy_setopt(curl, CURLOPT_INFILESIZE, static_cast<long>(read_object.len));
            }
            else {
                curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
                curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.data());
                curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size());
            }
        }
            break;
        case DELETE:
            curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
            break;
        default:
            break;
    }

    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, Client::write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &res);

    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, Client::header_callback);
    curl_easy_setopt(curl, CURLOPT_HEADERDATA, &res);

    curl_slist* hlist = nullptr;
    for (auto& header: req.headers)
        hlist = curl_slist_append(hlist, (header.first + ": " + header.second).data());
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hlist);

    if (timeout) {
        curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout);
        curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
    }

    auto cleanup = [=] {
        if (read_object.file)
            delete read_object.file;
        curl_slist_free_all(hlist);
        curl_easy_cleanup(curl);
        curl_global_cleanup();
    };

    auto r = curl_easy_perform(curl);
    if (r != CURLE_OK) {
        cleanup();
        throw runtime_error("http request error: " + to_string(r));
    }

    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &res.code);

    cleanup();
}

size_t Client::write_callback(void *data, size_t size, size_t nmemb, void *userdata) {
    auto r = static_cast<Response *>(userdata);
    size = size * nmemb;
    if (get_header(r->headers, "Content-Type") == "application/octet-stream") {
        if (!r->file || !r->file->write(data, size))
            return 0;  // leads to CURLE_WRITE_ERROR
    } else {
        string body(static_cast<const char *>(data), size);
        if (get_header(r->headers, "Content-Type") == "application/json")
            r->json = Json::parse(body);
        else
            r->body = std::move(body);
    }
    return size;
}

size_t Client::header_callback(void *data, size_t size, size_t nmemb, void *userdata) {
    auto r = static_cast<Response *>(userdata);
    string header(static_cast<char*>(data), size * nmemb);
    auto splits = str::split(header, ":");
    if (splits.size() == 2)
        r->headers.emplace(str::strip(splits[0]), str::strip(splits[1]));
    else
        r->headers.emplace(str::strip(splits[0]), "");
    return size * nmemb;
}

size_t Client::read_callback(void *data, size_t size, size_t nmemb, void *userdata) {
    auto r = static_cast<ReadObject*>(userdata);
    size = size * nmemb;
    if (r->len < size)
        size = r->len;
    if (r->data) {
        memcpy(data, r->data, size);
        r->data += size;
    }
    else {
        if (!r->file->read(data, size))
            return CURL_READFUNC_ABORT;  // leads to CURLE_ABORTED_BY_CALLBACK
    }
    r->len -= size;
    return size;
}

}}
